/* remote.--- Manages a client connection
    $Id: remote.js 52 2006-10-30 09:44:55Z luke $

  Copyright (C) 2006 by Massimiliano Mirra
  Copyright (C) 2006 by Lukas Loehrer

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA

  Author: lukas Loehrer <loehrerl [at] gmx [dot] net>
*/

/* Commentary:
This file implements a class that manages a client connection. For each client connection, an instance of this class is created. Incoming data is handled by the "_feed()" function. 

Functions intersting to other parts of the program are in the "Public functions" fold.
*/

const logger = module.require("package", "lib/filelogger").logger;
logger.debug("Tob of remote.js");



const json = logger.debugImport("json", function() { return module.require("package", "json_serialization"); });
const lisp = logger.debugImport("json", function() { return module.require("package", "lisp_serialization"); });

const util = module.require('package', 'util');
const services = module.require('package', 'service_manager');

// Services 
const testservice =module.require('package', 'services/test_service');
const shellservice = module.require("class", "services/jshell");
try {
	logger.debug("remote: about to load remoting");
	const remoting = module.require("class", "services/remoting_service");
logger.debug("remote: loaded remoting");
} catch (e) {
	logger.error("remote: failed to load remoting. " + e, e);
}

try {
	logger.debug("remote: about to load browsing");
	const browsing = module.require("class", "services/browser_service");
logger.debug("remote: loaded browsing");
} catch (e) {
	logger.error("remote: failed to load browsing. " + e, e);
}

function constructor(instream, outstream, server, context) {
    var _this = this;
	// The following is a kind of implicit logical time stamp for
	// outgoing messages. It is currently used to check for
	// consistency in garbage collection.
	this._sendCount = 0;

    this._instream = instream;
    this._outstream = outstream;
    this._server = server;

    this._creationContext = context;

    this._inputBuffer = '';
    this._networkListener = {
        onStartRequest: function(request, context) {},
        onStopRequest: function(request, context, status) {
            _this.quit();
        },
        onDataAvailable: function(request, context, inputStream, offset, count) {
            _this._feed(_this._instream.read(count));
        }
    }

	// Choose message serialization methods
	this._sizeHeaderLength= 6;

	// No serialization for testing
	// this._serializeServerMessage = util.identity;
	// this._parseClientMessage = util.identity;
	// JSON serialization
	this._unserializer = new json.Serializer();
	this._serializer = new lisp.Serializer();

	// Echo server for testing
	// this._processClientRequest = util.identity;

	// Remotely callable methods
	this._allowedRequests = {
		"js-call": this._invokeService
	};
	
	try {
		this._services = new services.ServiceManager();
		this._services.registerService(new testservice.TestService("TestService", this));
		this._services.registerService(new shellservice("JSEvaluation", this, context));
		this._services.registerService(new remoting("Remoting", this));
		this._services.registerService(new browsing("Browsing", this));
	} catch (e) {
		logger.error("remote.constructor: Error loading services. " + e, e);
	}

	logger.debug("remote.constructor: completed.");
}

// {{{ Public functions

// Setup special return value to signal that no return value is produced just now
// but will be later when an event occurs.
function returnLaterOnEvent(eventTarget, eventName, callback, useCapture, timeout) {
	timeout = timeout || 0;
	var e = new services.ReturnLaterOnEvent(eventTarget, eventName, callback, useCapture, timeout);
	return e;
}


function sendObject(obj) {
	logger.debug("remote.sendObj: About to serialize message.");
	var startTime = Date.now();
	var resultMsg = this._serializeServerMessage(obj);
	logger.profile(startTime, "Message serialization");
	logger.debug("remote.sendObj: About to send message of size " + resultMsg.length);
	startTime = Date.now();
	this._writeMessage(resultMsg);
	logger.profile(startTime, "Sending message");
	this._sendCount++;
}

// FF to emacs message creation
function makeJSReturnMessage(callID, exitStatus, result) {
	return {
		type: "js-return",
	callID: callID,
	exitStatus: exitStatus,
	result: result
	};
}

function makeJSEventMessage(serviceName, eventSpec, args) {
	var message = {
		type: "js-event",
		service: serviceName,
		spec: eventSpec,
		args: args
	};
	return message;
}


function makeJSErrorMessage(errorMessage, request) {
	return {
		type: "js-error",
	message: errorMessage
	};
}


function quit() {
	// Tell services that connection is closing
	this._services.disconnectServices();

    this._instream.close();
    this._outstream.close();
    this._server.removeSession(this);
	logger.debug("remote.quit: Disconnecting complete");
}

function getSerializer() {
	return this._serializer;
}

// }}}

	// {{{ Private functions

// Serilization
function _serializeServerMessage(obj) {
	return this._serializer.print(obj);
}

function _parseClientMessage(data) {
	return this._unserializer.read(data);
}

// Sets up the callback on the requested event and an optional timeout
function _setupReturnOnEvent(callId, eventTarget, eventName, callback, useCapture, timeout) {
	// Set up lexically scoped variables
	var _this = this;
	var _eventTarget = eventTarget;
	var _eventName = eventName;
	var _useCapture = useCapture;
	var _callback = callback;
	var _callId = callId;

	var wrappedCallback= function (event) {
		logger.debug("Return Later callback for call " + _callId);
		_eventTarget.removeEventListener(_eventName, arguments.callee, _useCapture);
		var resultMsg = null;
		try {
			var result = _callback(event);
			resultMsg = _this.makeJSReturnMessage(_callId, "EXIT_SUCCESS", result);
			
		} catch (e) {
			logger.error("Error in return later callback  for call " + _callId + ": " + e, e);
			// TOD: send error message back
		}
		_this.sendObject(resultMsg);
	};

	eventTarget.addEventListener(eventName, wrappedCallback, useCapture);

	if (timeout > 0) {
		var Handler = function () {
			_eventTarget.removeEventListener(_eventName, wrappedCallback, _useCapture);
			// TODO: return timeout error
		};
		setTimeout(timeout, handler);
	}
}

// Handler function for js-call messages
function _invokeService(request) {
	var result = null;
	try {
		var ts = this._services;
		var methodResult = ts.invokeService.apply(ts, request.args);
		if (methodResult instanceof services.ReturnLaterOnEvent) {
			this._setupReturnOnEvent(request.callID, methodResult.eventTarget, methodResult.eventName, methodResult.callback, methodResult.useCapture, methodResult.timeout);
			result = null;  // no result now
		} else {
			result = this.makeJSReturnMessage(request.callID, "EXIT_SUCCESS", methodResult);
		}
	} catch (e) {
		// TODO: Handle Errors
		logger.error("remote.invokeService: " + e, e);
		if (e instanceof services.NoSuchMethod) {
			result = this.makeJSReturnMessage(request.callID, "ERROR_NO_SUCH_METHOD", null);
		} else if (e instanceof services.NoSuchService) {
			result = this.makeJSReturnMessage(request.callID, "ERROR_NO_SUCH_SERVICE", null);
		} else if (e instanceof services.InvalidArgument){
			result = this.makeJSReturnMessage(request.callID, "ERROR_INVALID_ARGUMENT", e.toString());
}else {
			result = this.makeJSReturnMessage(request.callID, "ERROR_UNKNOWN", e.toString());
		}
	}
	return result;
}


function _processClientRequest(request) {
	var result = null;

	if (request.type in this._allowedRequests) {
		try {
			result = this._allowedRequests[request.type].call(this, request);
		} catch (e) {
			logger.error("remote._processRequest: " + e, e);
			// TODO: Implement proper error handling
			result = this.makeJSErrorMessage(e.message, request);
		}
	} else {
		logger.debug("Invalid request");
		result = this.makeJSErrorMessage("ERROR_INVALID_REQUEST_TYPE", request);
	}
	return result;
}

// Frame creation and sending
function _writeMessage(msg) {
	var msgBody = util.convertToUtf8(msg);
  var lengthString = this._lengthAsHex(msgBody.length);
  var frame = lengthString + msgBody;

  this._writeData(frame);
}

// Writes data to outstream
function _writeData(data) {
  var count;
  count = this._outstream.write(data, data.length);
  if (count != data.length) {
	logger.error("Short write " + count + " < " + data.length);
  }
  this._outstream.flush();
}

// Called when new data arrives from client
function _feed(input) {
  var msg, msgObject, resultObject, resultMsg;

  
  this._inputBuffer += input;
  try {
	while (msg = this._getNextMessage()) {
		logger.debug("remote._feed: Got message from client of length " + msg.length);
		msgObject = this._parseClientMessage(msg);
		logger.debug("Client message unserialized: " + msgObject);
		var result = this._processClientRequest(msgObject);
		if (result != null) {  // null means request gets no anser right now
			//logger.debug("Result object is:\n" + result.toSource());
		this.sendObject(result);
		} else {
			logger.debug("remote._feed: Call produced no immediate response.");
		}
	}
  }
  catch (e) {
	  logger.error("Remote._feed: " + e, e);
  }
}

function _getNextMessage() {
	var buf = this._inputBuffer;
	var headerLength = this._sizeHeaderLength;

		if  (buf.length >= headerLength) {
		var contentLength = this._chunkLength(buf);
		logger.debug("remote.getNextMessage buf.length = " + buf.length + " headerLength = " + headerLength + " contentLength " + contentLength);
		if (buf.length >= headerLength + contentLength)  {
			var content = buf.substring(headerLength, headerLength + contentLength);
			this._inputBuffer = buf.substring(headerLength + contentLength);
			return content;
		}
	}
	return null;
}

// decode and encode the packet length.
function _chunkLength (string) {
	var len =  parseInt (string.substring (0, this._sizeHeaderLength), 16);
	return len;
}

function _lengthAsHex (n) {
  var result = "";
  var bits = this._sizeHeaderLength * 4;
  for (var i = 0; i < bits; i += 4)
  {
	result = ((n >> i) & 0xf).toString (16) + result;
  }
    return result;
}

// }}}

		// {{{ Emacs local variables

// local variables:
// folded-file: t
// end:

// }}}

